package com.boot.dog.web.config.handler;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.db.sql.Condition;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import com.boot.dog.common.annotation.DataAuth;
import com.boot.dog.common.constant.SecurityConst;
import com.boot.dog.common.enums.DataAuthType;
import com.boot.dog.common.enums.UserType;
import com.boot.dog.common.auth.DogUser;
import com.boot.dog.common.util.SecurityUtil;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import javax.sql.DataSource;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;

/**
 * @Author: MaxLv
 * @Date: 2020/1/8
 */
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@AllArgsConstructor
public class DataAuthInterceptor extends AbstractSqlParserHandler implements Interceptor {

    private final DataSource dataSource;

    @Override
    @SneakyThrows
    public Object intercept(Invocation invocation) {
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        this.sqlParser(metaObject);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
/*        Object paramObj = boundSql.getParameterObject();
        IPage<?> page = null;
        if (paramObj instanceof IPage) {
            page = (IPage<?>) paramObj;
        } else if (paramObj instanceof Map) {
            for (Object arg : ((Map<?, ?>) paramObj).values()) {
                if (arg instanceof IPage) {
                    page = (IPage<?>) arg;
                    break;
                }
            }
        }
        if (null == page || page.getSize() < 0) {
            return invocation.proceed();
        }*/
        Method[] methods = Class.forName(StrUtil.subBefore(mappedStatement.getId(), ".", true)).getDeclaredMethods();
        String serverName = StrUtil.subAfter(mappedStatement.getId(), ".", true);
        DataAuth action = null;
        for (Method method : methods) {
            if (method.isAnnotationPresent(DataAuth.class) && serverName.equals(method.getName())) {
                action = method.getAnnotation(DataAuth.class);
                break;
            }
        }
        if (action == null) {
            return invocation.proceed();
        }
        DogUser user = SecurityUtil.getUser();
        //DogUser user = new DogUser("13586410419");
        Entity userEntity = Db.use(dataSource).queryOne("select * from tbl_user where deleted = 0 and username = '" + user.getUsername() + "'");
        String userType = userEntity.getStr("user_type");
        if ((SecurityConst.DATA_TYPE_ALL.equals(userEntity.getInt("data_type"))) || StrUtil.equals(userType, UserType.SUPER_ADMIN.code())) {
            return invocation.proceed();
        }
        PlainSelect plainSelect = (PlainSelect) ((Select) CCJSqlParserUtil.parse(boundSql.getSql())).getSelectBody();
        String whereSql = concatDataAuthSql(userEntity, action);
        if (StrUtil.isNotEmpty(whereSql)) {
            plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), new Parenthesis(CCJSqlParserUtil.parseCondExpression(whereSql))));
        }
        metaObject.setValue("delegate.boundSql.sql", plainSelect.toString());
        return invocation.proceed();

    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }

    @SneakyThrows
    private String concatDataAuthSql(Entity userEntity, DataAuth action) {
        String tableAlias = action.tableAlias();
        String fieldName = action.fieldName();
        if (DataAuthType.COMMON.equals(action.type())) {
            List<Integer> streetIds = Db.use(dataSource)
                    .findBy("tbl_user_area", new Condition("user_id", userEntity.getInt("id")), new Condition("deleted", 0))
                    .stream().map(entity -> entity.getInt("street_id"))
                    .distinct().collect(Collectors.toList());
            return tableAlias + "." + fieldName + " in (" + CollUtil.join(streetIds, ",") + ")";
        }
        if (DataAuthType.USER.equals(action.type())) {
            List<Entity> userAreaEntity = Db.use(dataSource)
                    .findBy("tbl_user_area", new Condition("user_id", userEntity.getInt("id")), new Condition("deleted", 0));
            List<Integer> provinceIds = userAreaEntity.stream().map(entity -> entity.getInt("province_id")).distinct().collect(Collectors.toList());
            List<Integer> cityIds = userAreaEntity.stream().map(entity -> entity.getInt("city_id")).distinct().collect(Collectors.toList());
            List<Integer> countyIds = userAreaEntity.stream().map(entity -> entity.getInt("county_id")).distinct().collect(Collectors.toList());
            List<Integer> streetIds = userAreaEntity.stream().map(entity -> entity.getInt("street_id")).distinct().collect(Collectors.toList());
            return "exists (select 1 from tbl_user_area t2 where t2.deleted = 0 and t2.user_id = " + tableAlias + ".id and t2.province_id in (" + CollUtil.join(provinceIds, ",") + ") and t2.city_id in (" + CollUtil.join(cityIds, ",") + ") and t2.county_id in (" + CollUtil.join(countyIds, ",") + ") and t2.street_id in (" + CollUtil.join(streetIds, ",") + "))";
        }
        return StrUtil.EMPTY;
    }

}